Skip to main content

7.2 - Common Pitfalls- Desyncs and Stale Data

Beyond simple syntax errors, bot development has its own class of insidious bugs. Two of the most common and difficult to diagnose are desyncs and errors caused by stale data. Understanding these pitfalls is essential for building a stable and reliable bot.


Pitfall 1: The Desync Bug

A "desync" (desynchronization) is a critical failure where your bot's perception of the game world no longer matches the reality of the game engine. Your bot is effectively "hallucinating"—seeing units that aren't there or missing units that are.

The Anatomy of a Desync:

+-----------------+                     +----------------+
| Your Bot's | (Commands are | |
| Internal State | too frequent | SC2 Game |
| (e.g., sees 10 | or complex) | Engine |
| marines) | -----------------> | (e.g., only |
+-----------------+ | has 5 marines)|
| +----------------+
| (State Mismatch)
v
CRITICAL FAILURE
(Bot issues commands for
units that don't exist)
  • Primary Cause: Sending an overwhelming number of commands to the game engine. This is almost always a symptom of inefficient code that spams redundant commands (see Chapter 7.1).
  • The Danger Zone: APM consistently in the tens of thousands is a major warning sign.
How to Avoid Desyncs
1. Follow Efficiency Principles: Write smart, non-redundant command logic.
2. Command Only What's Needed: Only issue commands to units whose state needs to change (e.g., .idle units).
3. Trust the Library's Batching: Simply call action methods (e.g., my_unit.attack(target)). The library automatically collects all commands within a step and sends them as one batch.
4. Monitor Your APM: Use self.state.score.apm to check your bot's APM. If it's abnormally high, it's a bug.

Pitfall 2: Stale Data

A "stale data" bug occurs when your bot makes a decision based on outdated information. The #1 cause of this is incorrectly storing a Unit object in a self variable between game steps.

The Problem: The Unit Object is a Temporary Snapshot The Unit objects you receive in self.units on each on_step are snapshots in time. They are valid only for that single game step. On the next step, the library creates a fresh set of Unit objects with updated data. A Unit object from a previous step is now "stale"—its position, health, and other data are wrong.

          on_step(iteration=100)
+------------------------------------+
| Unit A (tag=1, health=50, pos=X) | <-- You get this fresh object.
+------------------------------------+
| self.my_favorite_unit = Unit A | <-- DANGER: You store the object itself.
`------------------------------------`
|
v
on_step(iteration=101)
+------------------------------------+
| Unit A (tag=1, health=40, pos=Y) | <-- The library gets a new, updated object.
+------------------------------------+
| self.my_favorite_unit.health | <-- This still returns 50! It's stale.
`------------------------------------`

The Solution: Store Tags, Not Objects A unit's tag is a unique integer that is permanent for the entire match. It's the "serial number" for that unit.

The Golden Rule:

  • DO NOT store Unit or Units objects in self variables.
  • DO store a unit's .tag if you need to track it over time.

A Developer's Checklist for Data Persistence

  • Do I need to track a specific unit across multiple steps? (e.g., a scout, a specific spellcaster).
  • If yes, get its tag: self.scout_tag = my_scout.tag.
  • In later steps, retrieve the fresh unit object using the tag: current_scout = self.units.find_by_tag(self.scout_tag).
  • Always handle the case where the unit might be dead: if current_scout is None:.

Code Example: The Scout Tracker

This bot demonstrates the correct, professional way to track a specific unit. It designates a scout, stores its tag, and then correctly retrieves the fresh Unit object on each step to check its status.

# scout_tracker_bot.py
from typing import Optional

from sc2 import maps
from sc2.bot_ai import BotAI
from sc2.data import Difficulty, Race
from sc2.main import run_game
from sc2.player import Bot, Computer
from sc2.unit import Unit


class ScoutTrackerBot(BotAI):
"""Demonstrates the correct way to track a unit using its tag to avoid stale data."""

def __init__(self):
super().__init__()
# This will hold the scout's permanent ID, not a temporary object.
self.scout_tag: Optional[int] = None

async def on_step(self, iteration: int):
# 1. Designate the scout at the start of the game.
if iteration == 0:
# Select a worker to be our scout.
initial_scout_unit = self.workers.random
if initial_scout_unit:
# CORRECT: Store the integer tag.
self.scout_tag = initial_scout_unit.tag
print(f"Designated worker with tag {self.scout_tag} as the scout.")
# Send the scout to the enemy base.
initial_scout_unit.attack(self.enemy_start_locations[0])

# 2. Track the scout on every subsequent step.
if self.scout_tag is not None:
# CORRECT: Retrieve the fresh, up-to-date Unit object using the stored tag.
# This returns the unit object if it's visible, otherwise None.
current_scout: Optional[Unit] = self.units.find_by_tag(self.scout_tag)

if current_scout:
# The scout is alive and visible. We can safely use its current data.
if iteration % 112 == 0: # Throttle the log message.
print(f"Scout {self.scout_tag} is alive at {current_scout.position.rounded}.")
else:
# The scout is dead or has gone out of our vision.
print(f"Scout {self.scout_tag} has been lost. Resetting.")
# Reset the tag so we can assign a new scout if needed.
self.scout_tag = None


if __name__ == "__main__":
run_game(
maps.get("GresvanAIE"),
[
Bot(Race.Terran, ScoutTrackerBot()),
Computer(Race.Zerg, Difficulty.Easy)
],
realtime=True,
)